Create three files inside this folder:
📂 Suggested Folder: shared/models/authentication
AuthResponseDto.ts
export interface AuthResponseDto {
token: string;
phoneNumber: string;
roles: string[];
}
LoginRequestDto.ts
export interface LoginRequestDto {
phoneNumber: string;
password: string;
}
RegisterRequestDto.ts
export interface RegisterRequestDto {
phoneNumber: string;
password: string;
confirmPassword: string;
}
agent.ts
Auth
object to agent.ts
:const Auth = {
register: (data: RegisterRequestDto) =>
request.post<{ token: string }>('/auth/register', data),
login: (data: LoginRequestDto) =>
request.post<{ token: string }>('/auth/login', data),
};
agent.ts
ends like this:const agent = {
TransportationSearch,
Cities,
Auth
}
authStore.ts
Suggested Folder
📂 Suggested Folder: shared/store/
import { AuthResponseDto } from '@/shared/models/authentication/AuthResponseDto';
import {create} from 'zustand';
interface User {
phoneNumber: string;
roles: string[];
}
interface AuthState {
isLoggedIn: boolean;
user: User | null;
token: string | null;
login: (response: AuthResponseDto) => void;
logout: () => void;
setToken: (token: string) => void;
}
export const useAuthStore = create<AuthState>((set) => ({
isLoggedIn: false,
user: null,
token: null,
login: (response) =>
set(() => ({
token: response.token,
user: {
phoneNumber: response.phoneNumber,
roles: response.roles
},
isLoggedIn: true,
})),
logout: () =>
set(() => ({
token: null,
user: null,
isLoggedIn: false,
})),
setToken: (token) =>
set((state) => ({
token,
isLoggedIn: !!token,
user: state.user,
})),
}));
authStore.ts
This file defines a centralized authentication state store using Zustand
, a minimal and scalable state management library for React. It helps manage login state, user data, and authentication token across your application.
User
Interfaceinterface User {
phoneNumber: string;
roles: string[];
}
This interface defines the shape of the user
object stored in the auth state. It currently includes:
phoneNumber
: A string representing the user's phone number.roles
: An array representing user rolesAuthState
Interfaceinterface AuthState {
isLoggedIn: boolean;
user: User | null;
token: string | null;
login: (response: AuthResponseDto) => void;
logout: () => void;
setToken: (token: string) => void;
}
This defines the overall structure of the authentication store:
isLoggedIn
: Indicates whether a user is logged in.user
: Stores user-specific data if authenticated; otherwise null
.token
: JWT or access token from the server.login()
: Accepts an AuthResponseDto
and updates the state.logout()
: Clears all authentication-related data. setToken()
: Sets the token and toggles login status accordingly.export const useAuthStore = create<AuthState>((set) => ({ ... });
Creates a global auth store using Zustand. create()
accepts a function that receives set
(used to update state) and returns the initial store state and methods.
isLoggedIn: false,
user: null,
token: null,
These lines define the initial, default state for an unauthenticated user.
login()
Methodlogin: (response) =>
set(() => ({
token: response.token,
user: {
phoneNumber: response.phoneNumber,
roles: response.roles
},
isLoggedIn: true,
})),
AuthResponseDto
object after a successful login.token
, phoneNumber
, and roles
, and sets them in state.isLoggedIn: true
.logout()
Methodlogout: () =>
set(() => ({
token: null,
user: null,
isLoggedIn: false,
})),
isLoggedIn
to false
.setToken()
MethodsetToken: (token) =>
set((state) => ({
token,
isLoggedIn: !!token,
user: state.user,
})),
isLoggedIn
based on whether a non-empty token exists.user
object.📂 Suggested Folder: shared/features/authentication/modals
import React, { useState } from "react";
import { useAuthStore } from "@/store/authStore";
import agent from "@/shared/api/agent";
import { LoginRequestDto } from "@/shared/models/authentication/LoginRequestDto";
interface LoginModalProps {
onClose: () => void;
}
const LoginModal: React.FC<LoginModalProps> = ({ onClose }) => {
const login = useAuthStore((state) => state.login);
const [form, setForm] = useState<LoginRequestDto>({
phoneNumber: "",
password: "",
});
const [error, setError] = useState<string | null>(null);
const validate = () => {
const phoneRegex = /^(?:\+98|0)?9\d{9}$/;
if (!phoneRegex.test(form.phoneNumber)) {
return "Invalid phone number format";
}
if (!form.password || form.password.length < 8) {
return "Password must be at least 8 characters";
}
return null;
};
const handleSubmit = async () => {
const validationError = validate();
if (validationError) {
setError(validationError);
return;
}
try {
const response = await agent.Auth.login(form);
login(response);
setError(null);
onClose();
} catch (err: any) {
setError(err.response?.data?.message || "Login failed");
}
};
return (
<div style={styles.overlay}>
<div style={styles.modal}>
<h2 style={{ marginBottom: "1rem" }}>Login</h2>
<input
type="text"
placeholder="Phone Number"
value={form.phoneNumber}
onChange={(e) => setForm({ ...form, phoneNumber: e.target.value })}
style={styles.input}
/>
<input
type="password"
placeholder="Password"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
style={styles.input}
/>
<button onClick={handleSubmit} style={styles.button}>
Login
</button>
{error && (
<p style={{ color: "red", marginTop: "0.5rem", fontWeight: "bold" }}>
{error}
</p>
)}
<button
onClick={onClose}
style={{
...styles.button,
marginTop: "0.5rem",
backgroundColor: "#ccc",
color: "#333",
}}
>
Cancel
</button>
</div>
</div>
);
};
const styles: { [key: string]: React.CSSProperties } = {
//ADD STYLES
};
export default LoginModal;
This component provides a modal UI that allows users to log in using their phone number and password. It integrates with the authentication store and API to perform login logic and handle errors.
interface LoginModalProps {
onClose: () => void;
}
onClose
: A callback to be called when the modal should be closed (e.g., user clicks "Cancel" or logs in successfully).const LoginModal: React.FC<LoginModalProps> = ({ onClose }) => { ... };
Defines a functional React component with the onClose
prop destructured.
const login = useAuthStore((state) => state.login);
Retrieves the login
method from Zustand’s authStore
so that the global auth state can be updated after successful login.
const [form, setForm] = useState<LoginRequestDto>({
phoneNumber: "",
password: "",
});
Initializes form
state with empty values for the phone number and password.
const [error, setError] = useState<string | null>(null);
Stores any error messages resulting from validation or login attempt.
const validate = () => {
const phoneRegex = /^(?:\+98|0)?9\d{9}$/;
if (!phoneRegex.test(form.phoneNumber)) {
return "Invalid phone number format";
}
if (!form.password || form.password.length < 8) {
return "Password must be at least 8 characters";
}
return null;
};
null
if validation passes.const handleSubmit = async () => {
const validationError = validate();
if (validationError) {
setError(validationError);
return;
}
try {
const response = await agent.Auth.login(form);
login(response);
setError(null);
onClose();
} catch (err: any) {
setError(err.response?.data?.message || "Login failed");
}
};
validate()
and prevents submission if there's an error.agent.Auth.login()
.return (
<div style={styles.overlay}>
<div style={styles.modal}>
<h2>Login</h2>
<input ... />
<input ... />
<button onClick={handleSubmit}>Login</button>
{error && <p>{error}</p>}
<button onClick={onClose}>Cancel</button>
</div>
</div>
);
handleSubmit
.onClose
callback.const styles: { [key: string]: React.CSSProperties } = {
// Add modal styles here
};
This placeholder defines inline CSS styles for the modal. Each style (e.g., overlay
, modal
, input
, button
) should be defined here.
This modal:
import agent from "@/shared/api/agent";
import { RegisterRequestDto } from "@/shared/models/authentication/RegisterRequestDto";
import { useAuthStore } from "@/store/authStore";
import React, { useState } from "react";
interface Props {
onClose: () => void;
}
const RegisterModal: React.FC<Props> = ({ onClose }) => {
const [form, setForm] = useState<RegisterRequestDto>({
phoneNumber: "",
password: "",
confirmPassword: "",
});
const [error, setError] = useState<string | null>(null);
const login = useAuthStore((state) => state.login);
const validate = () => {
const { phoneNumber, password, confirmPassword } = form;
if (!phoneNumber || !password || !confirmPassword) {
return "All fields are required.";
}
if (!/^\d{11}$/.test(phoneNumber)) {
return "Phone number must be 11 digits.";
}
if (password.length < 6) {
return "Password must be at least 6 characters.";
}
if (password !== confirmPassword) {
return "Passwords do not match.";
}
return null;
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
const validationError = validate();
if (validationError) {
setError(validationError);
return;
}
try {
// Use the form as RegisterRequestDto explicitly
const requestData: RegisterRequestDto = {
phoneNumber: form.phoneNumber,
password: form.password,
confirmPassword: form.confirmPassword,
};
const response = await agent.Auth.register(requestData);
login(response);
setForm({ phoneNumber: "", password: "", confirmPassword: "" });
onClose();
} catch (err: any) {
setError(err.response?.data?.message || "Registration failed.");
}
};
return (
<div style={styles.overlay}>
<div style={styles.modal}>
<h2 style={{ marginBottom: "1rem" }}>Register</h2>
<form
onSubmit={handleSubmit}
style={{ display: "flex", flexDirection: "column" }}
>
<input
type="text"
name="phoneNumber"
value={form.phoneNumber}
onChange={handleChange}
placeholder="Phone Number"
style={styles.input}
/>
<input
type="password"
name="password"
value={form.password}
onChange={handleChange}
placeholder="Password"
style={styles.input}
/>
<input
type="password"
name="confirmPassword"
value={form.confirmPassword}
onChange={handleChange}
placeholder="Confirm Password"
style={styles.input}
/>
{error && (
<p
style={{ color: "red", marginTop: "0.5rem", fontWeight: "bold" }}
>
{error}
</p>
)}
<button type="submit" style={styles.button}>
Register
</button>
</form>
<button
onClick={onClose}
style={{
...styles.button,
marginTop: "0.5rem",
backgroundColor: "#ccc",
color: "#333",
}}
>
Cancel
</button>
</div>
</div>
);
};
const styles: { [key: string]: React.CSSProperties } = {
//ADD STYLES
};
export default RegisterModal;
interface Props {
onClose: () => void;
}
onClose
, a function to close the modal (e.g., hide it from the screen).const [form, setForm] = useState<RegisterRequestDto>({
phoneNumber: "",
password: "",
confirmPassword: "",
});
RegisterRequestDto
shape.const [error, setError] = useState<string | null>(null);
const login = useAuthStore((state) => state.login);
login
method from your global auth store, to automatically log in the user after successful registration.const validate = () => {
// Checks for empty fields
// Validates phone number format (must be 11 digits)
// Ensures password length is sufficient
// Confirms password and confirmation match
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
form
object dynamically based on the input name
.const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
const validationError = validate();
// If validation passes, submit the data to the backend
// If backend response is successful, log in and close modal
// If it fails, show error message
};
agent.Auth.register
<div style={styles.overlay}>...</div>
<form>
:phoneNumber
password
confirmPassword
onClose